﻿
using System;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
using System.Linq;
using System.Collections.Generic;

namespace Simulator
{
	partial class SheetArea
	{
		abstract class State
		{
			public abstract void OnMouseMove (MouseEventArgs e, out State newState);
			public abstract void OnMouseUp ();
			public virtual void OnPaint (Graphics g) { }
		}

		class StateBeginningDrag : State
		{
			const int Threshold = 7;

			readonly SheetArea _sheet;
			readonly MouseEventArgs _mouseDownEventArgs;

			public StateBeginningDrag (SheetArea sheet, MouseEventArgs mouseDownEventArgs)
			{
				_sheet = sheet;
				_mouseDownEventArgs = mouseDownEventArgs;
			}

			public override void OnMouseMove (MouseEventArgs e, out State newState)
			{
				if ((e.X >= _mouseDownEventArgs.X - Threshold)
					&& (e.X < _mouseDownEventArgs.X + Threshold)
					&& (e.Y >= _mouseDownEventArgs.Y - Threshold)
					&& (e.Y < _mouseDownEventArgs.Y + Threshold))
				{
					// didn't drag the required threshold
					newState = null;
					return;
				}

				// Check for click on a non-wired exterior port
				Port clickedPort = _sheet.GetExteriorPortAtCoords (_mouseDownEventArgs.X, _mouseDownEventArgs.Y, Port.SnappingRectangleSize);
				if (clickedPort != null)
				{
					if (_sheet.IsPortWired (clickedPort) == false)
					{
						// creating a new wire
						newState = new StateDrawingNewWire (_sheet, _mouseDownEventArgs, clickedPort);
						return;
					}
				}

				// Check for click on a wire point.

				Debug.Assert ((Port.SnappingRectangleSize % 2) == 0);

				foreach (var wire in _sheet._wires)
				{
					for (int pointIndex = 0; pointIndex < wire.Points.Count; pointIndex++)
					{
						PointF p = wire.Points [pointIndex].Location;

						if (_mouseDownEventArgs.X >= p.X - Port.SnappingRectangleSize / 2
							&& _mouseDownEventArgs.X < p.X + Port.SnappingRectangleSize / 2
							&& _mouseDownEventArgs.Y >= p.Y - Port.SnappingRectangleSize / 2
							&& _mouseDownEventArgs.Y < p.Y + Port.SnappingRectangleSize / 2)
						{
							newState = new StateMovingWirePoint (_sheet, _mouseDownEventArgs, wire, pointIndex);
							return;
						}
					}
				}

				// Breaking a wire?
				// TODO:

				int mouseX = _mouseDownEventArgs.X;
				int mouseY = _mouseDownEventArgs.Y;

				// check for click on a bridge corner
				foreach (Bridge bridge in _sheet._bridges)
				{
					// upper left
					if ((mouseX >= bridge.X)
						&& (mouseX < bridge.X + Bridge.GripSize)
						&& (mouseY >= bridge.Y)
						&& (mouseY < bridge.Y + Bridge.GripSize))
					{
						newState = new StateResizingBridge (_sheet, bridge, StateResizingBridge.Corner.UpperLeft, mouseX - bridge.X, mouseY - bridge.Y);
						return;
					}

					// upper right
					if ((mouseX >= bridge.X + bridge.Width - Bridge.GripSize)
						&& (mouseX < bridge.X + bridge.Width)
						&& (mouseY >= bridge.Y)
						&& (mouseY < bridge.Y + Bridge.GripSize))
					{
						newState = new StateResizingBridge (_sheet, bridge, StateResizingBridge.Corner.UpperRight, mouseX - (bridge.X + bridge.Width), mouseY - bridge.Y);
						return;
					}

					// lower left
					if ((mouseX >= bridge.X)
						&& (mouseX < bridge.X + Bridge.GripSize)
						&& (mouseY >= bridge.Y + bridge.Height - Bridge.GripSize)
						&& (mouseY < bridge.Y + bridge.Height))
					{
						newState = new StateResizingBridge (_sheet, bridge, StateResizingBridge.Corner.LowerLeft, mouseX - bridge.X, mouseY - (bridge.Y + bridge.Height));
						return;
					}

					// lower right
					if ((mouseX >= bridge.X + bridge.Width - Bridge.GripSize)
						&& (mouseX < bridge.X + bridge.Width)
						&& (mouseY >= bridge.Y + bridge.Height - Bridge.GripSize)
						&& (mouseY < bridge.Y + bridge.Height))
					{
						newState = new StateResizingBridge (_sheet, bridge, StateResizingBridge.Corner.LowerRight, mouseX - (bridge.X + bridge.Width), mouseY - (bridge.Y + bridge.Height));
						return;
					}
				}

				if (_sheet._selectedBridge != null)
				{
					if (_sheet._selectedPortIndex == -1)
					{
						newState = new StateDraggingBridge (_sheet, _mouseDownEventArgs.X, _mouseDownEventArgs.Y);
						return;
					}
					else
					{
						newState = new StateMovingInteriorPort (_sheet, _sheet._selectedBridge.Ports [_sheet._selectedPortIndex]);
						return;
					}
				}

				newState = null;
				return;
			}

			public override void OnMouseUp ()
			{
			}
		}

		class StateDrawingNewWire : State
		{
			readonly SheetArea _sheet;
			readonly MouseEventArgs _mouseDownEventArgs;
			readonly Port _startPort;
			Port _endPort;
			PointF _endLooseLocation;

			public StateDrawingNewWire (SheetArea sheet, MouseEventArgs mouseDownEventArgs, Port startPort)
			{
				_sheet = sheet;
				_mouseDownEventArgs = mouseDownEventArgs;
				_startPort = startPort;
				_endPort = null;
				_endLooseLocation = new PointF (100, 100);
			}

			public override void OnMouseMove (MouseEventArgs e, out State newState)
			{
				Port newEndPort = _sheet.GetExteriorPortAtCoords (e.X, e.Y, Port.SnappingRectangleSize);

				if (newEndPort != null)
				{
					// end snapping to a port

					if (_sheet._wires.SelectMany (w => w.Points).OfType<WirePointPort> ().Any (wpp => wpp.Port == newEndPort))
					{
						// the mouse is over an already connected port
						newEndPort = null;
					}

					if (newEndPort != _endPort)
					{
						// end snapping to a NEW port
						_endPort = newEndPort;
						_sheet.Invalidate ();
					}
				}
				else
				{
					// end loose
					_endPort = null;
					_endLooseLocation = new PointF (e.X, e.Y);
					_sheet.Invalidate ();
				}

				newState = null;
			}

			public override void OnMouseUp ()
			{
				if (_startPort == _endPort)
				{
					// Zero-length wire. We don't create it.
				}
				else if (_endPort != null)
				{
					// The wire ends at another port (not in the air).

					Wire newWire = new Wire (_startPort, _endPort);

					lock (_sheet._wires)
					{
						_sheet._wires.Add (newWire);
					}

					_sheet._hintControl.HideHint (HintFirstBridgeDrawWires);
				}
				else
				{
					// The wire ends in the air. Dont' allow it.
				}

				_sheet.Invalidate ();
			}

			public override void OnPaint (Graphics g)
			{
				base.OnPaint (g);

				// Paint the wire being created.
				PointF start = _startPort.ConnectorLocation;
				PointF end;
				if (_endPort != null)
					end = _endPort.ConnectorLocation;
				else
				{
					end = _endLooseLocation;
				}

				g.DrawLine (Wire.TempWirePen, start, end);
			}
		}

		class StateMovingWirePoint : State
		{
			readonly SheetArea _sheet;
			readonly MouseEventArgs _mouseDownEventArgs;
			readonly Wire _wire;
			readonly int _pointIndex;
			Port _snappingPort;
			Point _loosePointLocation;

			public StateMovingWirePoint (SheetArea sheet, MouseEventArgs mouseDownEventArgs, Wire wire, int pointIndex)
			{
				_sheet = sheet;
				_mouseDownEventArgs = mouseDownEventArgs;
				_wire = wire;
				_pointIndex = pointIndex;

				if (wire.Points [pointIndex] is WirePointPort)
					_snappingPort = (wire.Points [pointIndex] as WirePointPort).Port;
				else
					_loosePointLocation = wire.Points [pointIndex].Location;
			}

			public override void OnMouseMove (MouseEventArgs e, out State newState)
			{
				Port newSnappingPort = _sheet.GetExteriorPortAtCoords (e.X, e.Y, Port.SnappingRectangleSize);
				if (newSnappingPort != null)
				{
					// Mouse is on top of a port.
					// Only allow if we are dragging the start or the end of the wire.
					if ((_pointIndex == 0) || (_pointIndex == _wire.Points.Count - 1))
					{
						// and either:
						// - the port is free, or
						// - the port is already wired, and it is wired to the point we are currently dragging.
						if ((_sheet.IsPortWired (newSnappingPort) == false)
							|| (_wire.Points [_pointIndex] is WirePointPort) && ((_wire.Points [_pointIndex] as WirePointPort).Port == newSnappingPort))
						{
							// Did we just snap to this port?
							if (_snappingPort != newSnappingPort)
							{
								// Yes, we've just snapped.
								_snappingPort = newSnappingPort;
								_loosePointLocation = _snappingPort.ConnectorLocation;
								_sheet.Invalidate ();
							}
						}
					}
				}
				else
				{
					// Mouse is in air.

					// Did we just unsnap?
					if (_wire.Points [_pointIndex] is WirePointPort)
					{
						// Yes, we've just unsnapped.
						_snappingPort = null;
					}

					_loosePointLocation.X = e.X;
					_loosePointLocation.Y = e.Y;
					_sheet.Invalidate ();
				}

				newState = null;
			}

			public override void OnMouseUp ()
			{
				WirePoint oldPoint;
				WirePoint otherPoint;
				if (_pointIndex == 0)
				{
					oldPoint = _wire.Points [0];
					otherPoint = _wire.Points [_wire.Points.Count - 1];
				}
				else if (_pointIndex == _wire.Points.Count - 1)
				{
					oldPoint = _wire.Points [_wire.Points.Count - 1];
					otherPoint = _wire.Points [0];
				}
				else
					// moving middle point
					throw new NotImplementedException ();

				uint timestamp = (uint) Environment.TickCount;

				if (_snappingPort != null)
				{
					// The point is now dragged to a port.
					var newPortPoint = new WirePointPort (_wire, _snappingPort);

					// Was the dragged point snapped to a port before? (or we just snapped it?)
					if (oldPoint is WirePointPort)
					{
						// Yes. The dragged point was snapped before too.

						Port oldPort = (oldPoint as WirePointPort).Port;

						// If we snap the point to the same port it was snapped before, we don't treat it differently;
						// we disconnect and reconnect the wire, like we do in case of snapping to a different port.
						// This helps testing exactly such a scenario (cable disconnected and reconnected to same port).
						// (If canceling is instead desired, it can be done by pressing Escape, in which case nothing is disconnected/reconnected.)

						// Was the other end snapped too?
						if (otherPoint is WirePointPort)
						{
							// Yes, the other end was snapped to a port too.

							// We disconnected and reconnected a wire. In a real system this always leads to ports
							// going down and then up again, long enough for the software to notice and inform the STP library.
							//
							// Let's simulate this behavior here: we'll disconnect the wire, tell STP about ports having gone down,
							// wait for STP to finish its processing, and then reconnect the wire (which STP will find about a bit later).

							Port otherPort = (otherPoint as WirePointPort).Port;

							// Disable all affected ports.
							HashSet<Port> affectedPorts = new HashSet<Port> { otherPort, _snappingPort, oldPort };
							foreach (var port in affectedPorts)
							{
								port.MAC_Operational = false;

								lock (port.Bridge.WrapperLock)
								{
									if (port.Bridge.Wrapper.GetPortEnabled (port.PortIndex))
										port.Bridge.Wrapper.OnPortDisabled (port.PortIndex, timestamp);
								}
							}
						}

						// Replace the wire point if snapped to a different port.
						if (oldPort != _snappingPort)
						{
							lock (_sheet._wires)
							{
								_wire.Points [_pointIndex] = newPortPoint;
							}
						}

						// If we disconnected ports above, the code of the periodic timer will reenable them a bit later.
					}
					else
					{
						// The dragged wire end was in the air before. We've just snapped it to this port.
						// Let's make the dragged end point to the snapping port.
						lock (_sheet._wires)
						{
							_wire.Points [_pointIndex] = newPortPoint;
						}
					}
				}
				else
				{
					// The dragged point is now in air.
					var newFreePoint = new WirePointFree (_wire, _loosePointLocation);

					// Was it snapped to a port before?
					if (oldPoint is WirePointPort)
					{
						// Yes, it was. Disable that port.
						Port oldPort = (oldPoint as WirePointPort).Port;
						oldPort.MAC_Operational = false;
						lock (oldPort.Bridge.WrapperLock)
						{
							if (oldPort.Bridge.Wrapper.GetPortEnabled (oldPort.PortIndex))
								oldPort.Bridge.Wrapper.OnPortDisabled (oldPort.PortIndex, timestamp);
						}
					}

					// Is the other end of this wire snapped to a port?
					if (otherPoint is WirePointPort)
					{
						// Yes, the other end was snapped to a port. Disable it.
						Port otherPort = (otherPoint as WirePointPort).Port;
						otherPort.MAC_Operational = false;
						lock (otherPort.Bridge.WrapperLock)
						{
							if (otherPort.Bridge.Wrapper.GetPortEnabled (otherPort.PortIndex))
								otherPort.Bridge.Wrapper.OnPortDisabled (otherPort.PortIndex, timestamp);
						}
					}

					// Now replace the old wire point with the new one.
					lock (_sheet._wires)
					{
						_wire.Points [_pointIndex] = newFreePoint;
					}
				}

				_sheet.Invalidate ();
			}

			public override void OnPaint (Graphics g)
			{
				base.OnPaint (g);

				// Paint the sections of the wire that are to the left and to the right of the point that is being moved.
				if (_pointIndex > 0)
				{
					// This is not the first point, so we can paint the section on the left.
					PointF start = _wire.Points [_pointIndex - 1].Location;

					g.DrawLine (Wire.TempWirePen, start, _loosePointLocation);
				}

				if (_pointIndex < _wire.Points.Count - 1)
				{
					// This is not the last point, so we can paint the section on the right.
					PointF end = _wire.Points [_pointIndex + 1].Location;

					g.DrawLine (Wire.TempWirePen, _loosePointLocation, end);
				}
			}
		}

		class StateResizingBridge : State
		{
			public enum Corner
			{
				UpperLeft,
				UpperRight,
				LowerLeft,
				LowerRight
			}

			readonly SheetArea _sheet;
			readonly Bridge _bridge;
			readonly Corner _corner;
			// mouse coords relative to corner coords (i.e., _offsetX is positive when mouse is at the right of the corner)
			readonly int _offsetX;
			readonly int _offsetY;

			public StateResizingBridge (SheetArea sheet, Bridge bridge, Corner corner, int offsetX, int offsetY)
			{
				_sheet = sheet;
				_bridge = bridge;
				_corner = corner;
				_offsetX = offsetX;
				_offsetY = offsetY;
			}

			public override void OnMouseMove (MouseEventArgs e, out State newState)
			{
				int newX, newY, newWidth, newHeight;
				if ((_corner == Corner.UpperLeft) || (_corner == Corner.LowerLeft))
				{
					newX = e.X - _offsetX;
					newWidth = _bridge.X + _bridge.Width - newX;
				}
				else // if ((_corner == Corner.UpperRight) || (_corner == Corner.LowerRight))
				{
					newX = _bridge.X;
					newWidth = e.X - _offsetX - _bridge.X;
				}

				if ((_corner == Corner.UpperLeft) || (_corner == Corner.UpperRight))
				{
					newY = e.Y - _offsetY;
					newHeight = _bridge.Y + _bridge.Height - newY;
				}
				else // if ((_corner == Corner.LowerLeft) || (_corner == Corner.LowerRight))
				{
					newY = _bridge.Y;
					newHeight = e.Y - _offsetY - _bridge.Y;
				}

				// check that no ports are left outside
				bool allow = true;
				foreach (Port port in _bridge.Ports)
				{
					if ((port.Side == Side.Top) || (port.Side == Side.Bottom))
					{
						if ((port.Offset < Port.InteriorDrawingLongSize / 2) || (port.Offset > newWidth - Port.InteriorDrawingLongSize / 2))
						{
							allow = false;
							break;
						}
					}
					else // left or right
					{
						if ((port.Offset < Port.InteriorDrawingLongSize / 2) || (port.Offset > newHeight - Port.InteriorDrawingLongSize / 2))
						{
							allow = false;
							break;
						}
					}
				}

				if (newWidth < 100)
					allow = false;

				if (newHeight < 50)
					allow = false;

				if (allow)
				{
					_bridge.X = newX;
					_bridge.Y = newY;
					_bridge.Width = newWidth;
					_bridge.Height = newHeight;
				}

				_sheet.Invalidate ();

				newState = null;
			}

			public override void OnMouseUp ()
			{
			}
		}

		class StateDraggingBridge : State
		{
			readonly SheetArea _sheet;
			int _lastMouseX;
			int _lastMouseY;

			public StateDraggingBridge (SheetArea sheet, int lastMouseX, int lastMouseY)
			{
				_sheet = sheet;
				_lastMouseX = lastMouseX;
				_lastMouseY = lastMouseY;
			}

			public override void OnMouseMove (MouseEventArgs e, out State newState)
			{
				_sheet._selectedBridge.X += (e.X - _lastMouseX);
				_sheet._selectedBridge.Y += (e.Y - _lastMouseY);
				_sheet.Invalidate ();
				_lastMouseX = e.X;
				_lastMouseY = e.Y;
				newState = null;
			}

			public override void OnMouseUp ()
			{
				_sheet._hintControl.HideHint (HintFirstBridgeMove);

				_sheet._hintControl.ShowHint (HintMovePorts);
			}
		}

		class StateMovingInteriorPort : State
		{
			readonly SheetArea _sheet;
			readonly Port _port;

			public StateMovingInteriorPort (SheetArea sheet, Port port)
			{
				_sheet = sheet;
				_port = port;
			}

			public override void OnMouseMove (MouseEventArgs e, out State newState)
			{
				_port.Bridge.SetCoordsForInteriorPort (_port, e.X, e.Y);
				_sheet.Invalidate ();
				newState = null;
			}

			public override void OnMouseUp ()
			{
				_sheet._hintControl.HideHint (HintMovePorts);
			}
		}
	}
}